package com.testing.sunnycommon.automation;

import com.google.common.io.Files;
import com.testing.sunnycommon.automation.annotations.KeyDescription;
import com.testing.sunnycommon.automation.annotations.KeyParams;
import com.testing.sunnycommon.automation.annotations.KeyUsage;
import com.testing.sunnycommon.automation.model.AutoResult;
import com.testing.sunnycommon.model.SunnyVariable;
import com.testing.sunnycommon.service.SunnyVariableService;
import lombok.extern.slf4j.Slf4j;
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.Select;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.Date;
import java.util.Set;

/**
 * @Classname WebKeywords
 * @Description 类型说明
 * @Date 2023/5/15 17:28
 * @Created by 特斯汀Roy
 */
@Slf4j
@Component
public class WebKeywords {

    @Resource
    SunnyVariableService sunnyVariableService;

    //将driver对象作为成员变量，方便关键字中的方法来进行调用。
    WebDriver driver;

    @KeyUsage("启动浏览器，目前只支持chrome浏览器")
    @KeyDescription("通过参数传入chromedriver路径")
    public AutoResult openBrowser(@KeyParams(name = "driverPath", value = "chromedriver的路径") String driverPath) {
        try {
            System.setProperty("webdriver.chrome.driver", driverPath);
            System.setProperty("webdriver.chrome.silentOutput", "true");
            ChromeOptions chromeOptions = new ChromeOptions();
            //设置自动化启动时，不显示正在受到自动化软件控制的提示栏
            chromeOptions.setExperimentalOption("excludeSwitches", new String[]{"enable-automation", "load-extension"});
            //针对反爬机制，把浏览器不标记为webdriver启动的浏览器。
            chromeOptions.addArguments("--disable-blink-features=AutomationControlled");
            //处理110版本之后的连接403报错
            chromeOptions.addArguments("--remote-allow-origins=*");
            driver = new ChromeDriver(chromeOptions);
            //添加隐式等待
            driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
            log.info("正在启动浏览器");
            return AutoResult.pass("启动浏览器成功");
        } catch (Exception e) {
            log.error("启动浏览器失败", e.fillInStackTrace());
            return AutoResult.fail("启动浏览器失败", "");
        }
    }

    /**
     * 浏览器访问网页
     *
     * @param url 网页url地址
     */
    @KeyUsage("访问网页")
    @KeyDescription("浏览器访问网页")
    public AutoResult visitWeb(@KeyParams(name = "url", value = "网页url") String url) {
        try {
            driver.get(url);
            log.info("浏览器访问网页:" + url);
            return AutoResult.pass("访问网页" + url + "成功");
        } catch (Exception e) {
            log.error("访问网页" + url + "失败");
            return AutoResult.fail("访问网页" + url + "失败", "");
        }
    }

    @KeyUsage("强制等待")
    @KeyDescription("固定等待指定时长")
    public AutoResult halt(@KeyParams(name = "seconds", value = "等待时长，单位为秒，支持小数") String seconds) {
        try {
            float fSeconds = Float.parseFloat(seconds);
            Thread.sleep((int) (fSeconds * 1000));
            return AutoResult.pass("强制等待" + seconds + "秒成功");
        } catch (Exception e) {
            log.error("强制等待失败", e.fillInStackTrace());
            return AutoResult.fail("强制等待失败", e.fillInStackTrace().toString());
        }
    }

    /**
     * 定位元素方法，由于外部不需要使用，所以作为private方法
     *
     * @param locator
     * @return
     */
    private WebElement locateElement(String locator) {
        try {
            WebElement element;
            if (locator.startsWith("/") || locator.startsWith("(")) {
                //xpath可能的表达式  //Ele[条件]  或者 (表达式)[下标]
                element = driver.findElement(By.xpath(locator));
            } else if (locator.startsWith(".") || locator.startsWith("#") || locator.contains("[")) {
                //css选择器表达式
                element = driver.findElement(By.cssSelector(locator));
            } else {
                //id类型的定位表达式，没有任何特征
                element = driver.findElement(By.id(locator));
            }
            //将定位的元素背景颜色改为紫色
            JavascriptExecutor runner = (JavascriptExecutor) driver;
            runner.executeScript("arguments[0].style.backgroundColor='purple'", element);
            return element;
        } catch (Exception e) {
            log.error("定位" + locator + "元素失败了", e.fillInStackTrace());
            return null;
        }
    }

    @KeyUsage("鼠标悬停到指定页面元素")
    @KeyDescription("鼠标悬停到指定页面元素")
    public AutoResult hover(@KeyParams(name = "locator", value = "悬停元素的定位表达式,支持xpath、css和id三种定位方式，在定位时：\\n" +
            "1、如果以/或(开头，认为是xpath \\n " +
            "2、如果以#或.开头或包含[认为是css选择器 \\n" +
            "3、其余情况认为是id \\n") String locator) {
        if (driver == null) {
            log.error("浏览器未启动");
            return AutoResult.skipped("浏览器未启动，跳过");
        }
        try {
            Actions actions = new Actions(driver);
            actions.moveToElement(locateElement(locator)).perform();
            log.info("鼠标悬停到元素" + locator);
            return AutoResult.pass("鼠标悬停到元素" + locator);
        } catch (Exception e) {
            log.error("鼠标悬停到元素" + locator + "失败");
            return AutoResult.fail("鼠标悬停到元素" + locator + "失败", takeScreenShot("悬停元素失败"));
        }
    }

    @KeyUsage("在selenium中执行js脚本")
    @KeyDescription("在selenium中")
    public AutoResult execJS(@KeyParams(name = "jsScript", value = "要执行的js脚本") String jsScript) {
        if (driver == null) {
            log.error("浏览器未启动");
            return AutoResult.skipped("浏览器未启动，跳过");
        }
        try {
            JavascriptExecutor runner = (JavascriptExecutor) driver;
            Object result = runner.executeScript(jsScript);
            log.info("执行js脚本的结果是" + result.toString());
            return AutoResult.pass("执行js脚本成功，结果是" + result.toString());
        } catch (Exception e) {
            log.error("执行js脚本失败", e.fillInStackTrace());
            return AutoResult.fail("执行js脚本失败", takeScreenShot("js脚本执行失败"));
        }
    }


    /**
     * 执行错误的时候进行浏览器截图
     *
     * @param shotFilename
     */
    private String takeScreenShot(String shotFilename) {
        if (driver == null) return "";
        try {
            //完成截图操作，存在内存中的文件
            File screenshotAs = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
            //创建一个文件系统中的真实文件
            //按指定的日期格式，生成时间戳字符串
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH：mm：ss");
            String timestamp = simpleDateFormat.format(new Date());
            String fileName = "auto-logs/screenshots/" + shotFilename + timestamp + ".png";
            File shotFile = new File(fileName);
            //把截图的内存内容写到真实文件中
            Files.copy(screenshotAs, shotFile);
            log.error(shotFilename + "截图完成");
            return fileName;
        } catch (IOException e) {
            log.error("截图失败", e.fillInStackTrace());
            return "";
        }
    }

    @KeyUsage("点击元素")
    @KeyDescription("输入待点击元素的定位表达式，完成元素点击")
    public AutoResult click(@KeyParams(name = "locator", value = "支持xpath、css和id三种定位方式，在定位时：\\n" +
            "1、如果以/或(开头，认为是xpath \\n " +
            "2、如果以#或.开头或包含[认为是css选择器 \\n" +
            "3、其余情况认为是id \\n") String locator) {
        if (driver == null) {
            log.error("浏览器未启动");
            return AutoResult.skipped("浏览器未启动，跳过");
        }
        if (locateElement(locator) != null) {
            try {
                locateElement(locator).click();
                log.info("点击了" + locator + "元素");
                return AutoResult.pass("点击了" + locator + "元素");
            } catch (Exception e) {
                log.error("元素点击失败", e.fillInStackTrace());
                String fileName = takeScreenShot("元素点击失败");
                return AutoResult.fail("元素点击失败", fileName);
            }
        } else {
            log.error("没有定位到" + locator + "所代表的元素，点击取消");
            String fileName = takeScreenShot("定位失败");
            return AutoResult.fail("定位要点击的元素失败", fileName);
        }
    }

    @KeyUsage("输入内容")
    @KeyDescription("填写定位表达式并输入指定内容")
    public AutoResult input(@KeyParams(name = "locator", value = "支持xpath、css和id三种定位方式，在定位时：" +
            "1、如果以/或(开头，认为是xpath \\n " +
            "2、如果以#或.开头或包含[认为是css选择器 \\n" +
            "3、其余情况认为是id \\n") String locator, @KeyParams(name = "content", value = "向输入框输入的内容") String content) {
        if (driver == null) {
            log.error("浏览器未启动");
            return AutoResult.skipped("浏览器未启动，跳过");
        }
        if (locateElement(locator) != null) {
            try {
                locateElement(locator).sendKeys(content);
                log.info("向" + locator + "元素输入了" + content);
                return AutoResult.pass("向" + locator + "元素输入了" + content);
            } catch (Exception e) {
                log.error(locator + "元素输入失败", e.fillInStackTrace());
                String filename = takeScreenShot("输入失败");
                return AutoResult.fail(locator + "元素输入失败", filename);
            }
        } else {
            log.error("没有定位到所代表的元素，输入取消");
            String filename = takeScreenShot("定位失败");
            return AutoResult.fail("定位要输入的元素失败", filename);
        }
    }

    @KeyUsage("完成select下拉框元素的选中")
    @KeyDescription("通过下拉框中的显示内容选中下拉列表")
    public AutoResult selectByVisibleText(@KeyParams(name = "locator", value = "获取select下拉框的定位表达式，支持xpath、css和id三种定位方式，在定位时：" +
            "1、如果以/或(开头，认为是xpath \\n " +
            "2、如果以#或.开头或包含[认为是css选择器 \\n" +
            "3、其余情况认为是id \\n") String locator, @KeyParams(name = "text", value = "要选中元素的显示文本") String text) {
        if (driver == null) {
            log.error("浏览器未启动");
            return AutoResult.skipped("浏览器未启动，跳过");
        }
        try {
            Select select = new Select(locateElement(locator));
            select.selectByVisibleText(text);
            halt("0.5");
            log.info("基于text:" + text + "选中下拉列表" + locator + "中的选项");
            return AutoResult.pass("基于text:" + text + "选中下拉列表" + locator + "中的选项");
        } catch (Exception e) {
            log.error("基于text:" + text + "选中下拉列表" + locator + "中的选项失败");
            return AutoResult.fail("基于text:" + text + "选中下拉列表" + locator + "中的选项失败",
                    takeScreenShot("select失败"));
        }
    }

    @KeyUsage("切换iframe")
    @KeyDescription("切换到指定元素的Iframe中")
    public AutoResult switchIframe(@KeyParams(name = "locator", value = "切换Iframe时的定位表达式，支持xpath、css和id三种定位方式，在定位时：" +
            "1、如果以/或(开头，认为是xpath \\n " +
            "2、如果以#或.开头或包含[认为是css选择器 \\n" +
            "3、其余情况认为是id \\n") String locator) {
        if (driver == null) {
            log.error("浏览器未启动");
            return AutoResult.skipped("浏览器未启动，跳过");
        }
        try {
            driver.switchTo().frame(locateElement(locator));
            log.info("切换到Iframe：" + locator);
            return AutoResult.pass("切换Iframe成功");
        } catch (Exception e) {
            log.error("切换到iframe" + locator + "失败", e.fillInStackTrace());
            String filename = takeScreenShot("切换Iframe失败");
            return AutoResult.fail("切换IFrame元素失败", filename);
        }
    }


    @KeyUsage("切换浏览器窗口进行操作")
    @KeyDescription("基于浏览器打开窗口进行遍历，找到标题包含指定内容的窗口切换过去进行操作")
    public AutoResult switchWindow(@KeyParams(name = "windowTitle", value = "要切换到的窗口的标题中要包含的内容") String windowTitle) {
        try {
            Set<String> windowHandles = driver.getWindowHandles();
            for (String windowHandle : windowHandles) {
                //            找到每个句柄，切换到对应的窗口
                driver.switchTo().window(windowHandle);
                //如果包含的话，就已经找到了，不继续切换
                if (driver.getTitle().contains(windowTitle)) {
                    break;
                }
            }
            return AutoResult.pass("切换到标题为" + windowTitle + "的窗口成功");
        } catch (Exception e) {
            log.error("切换到标题为" + windowTitle + "的窗口失败", e.fillInStackTrace());
            return AutoResult.fail("切换到标题为" + windowTitle + "的窗口失败", takeScreenShot("切换窗口失败"));
        }
    }


    @KeyUsage("切换上层Ifarme")
    @KeyDescription("切换上层Ifarme")
    public AutoResult switchParentFrame() {
        if (driver == null) {
            log.error("浏览器未启动");
            return AutoResult.skipped("浏览器未启动，跳过");
        }
        try {
            driver.switchTo().parentFrame();
            return AutoResult.pass("切换到上层Iframe成功");
        } catch (Exception e) {
            log.error("切换到上层Iframe失败", e.fillInStackTrace());
            return AutoResult.fail("切换到上层Iframe失败", takeScreenShot("切换上层Iframe失败"));
        }
    }


    @KeyUsage("断言页面包含指定内容")
    @KeyDescription("断言页面上的某个元素的文本内容包含预期值")
    public AutoResult assertElementContainsText(@KeyParams(name = "locator", value = "待断言元素定位表达式，支持xpath、css和id三种定位方式，在定位时：" +
            "1、如果以/或(开头，认为是xpath \\n " +
            "2、如果以#或.开头或包含[认为是css选择器 \\n" +
            "3、其余情况认为是id \\n") String locator, @KeyParams(name = "expectedText", value = "断言元素文本中要包含的预期内容") String expectedText) {
        try {
            String actualText = locateElement(locator).getText();
            if (actualText.contains(expectedText)) {
                log.info("元素" + locator + "的实际内容是" + actualText + "，其中包含" + expectedText);
                return AutoResult.pass("元素" + locator + "的实际内容是" + actualText + "，其中包含" + expectedText + "，断言成功");
            } else {
                log.error("元素" + locator + "的实际内容是" + actualText + "，其中不包含" + expectedText);
                return AutoResult.fail("元素" + locator + "的实际内容是" + actualText + "，其中不包含" + expectedText + "，断言失败", takeScreenShot("断言失败"));
            }
        } catch (Exception e) {
            log.error("断言元素" + locator + "包含内容失败", e.fillInStackTrace());
            return AutoResult.fail("断言元素" + locator + "包含内容失败", takeScreenShot("断言失败"));
        }
    }

    @KeyUsage("关闭浏览器")
    @KeyDescription("关闭浏览器，并且结束chromedriver.exe进程")
    public AutoResult closeBrowser() {
        log.info("关闭浏览器……");
        driver.quit();
        return AutoResult.pass("关闭浏览器");
    }

    /*********************************参数操作********************************************/
    @KeyUsage("在自动化执行过程中存储参数")
    @KeyDescription("在自动化执行过程中存储参数")
    public AutoResult saveVariable(String varName, String varValue) {
        //创建好参数对象
        SunnyVariable variable = new SunnyVariable();
        variable.setId(0L);
        variable.setProjectId(1L);
        variable.setVarName(varName);
        variable.setVarValue(varValue);
        boolean result = sunnyVariableService.saveOrUpdateVariable(variable);
        log.info("存储参数成功，存储的参数是  " + varName + "：" + varValue);
        return result ? AutoResult.pass("存储参数成功，存储的参数是  " + varName + "：" + varValue) : AutoResult.fail("存储参数失败，请检查是否传入了不合适的id", "");
    }


}
